package fudan.se.project.service.infrastructure.func.conference.resoure.paperalloc;

import fudan.se.project.domain.conference.Conference;
import fudan.se.project.domain.conference.production.Paper;
import fudan.se.project.domain.conference.production.ReviewInfo;
import fudan.se.project.domain.conference.production.Writer;
import fudan.se.project.domain.conference.role.PcMember;
import fudan.se.project.repository.entity.conference.production.ReviewInfoRepository;

import java.util.*;

class PaperAllocatorBalanced extends AbstractPaperAllocator {

    private ReviewInfoRepository reviewInfoRepository;

    public PaperAllocatorBalanced(int assignedCount) {
        super(assignedCount);
    }

    public PaperAllocatorBalanced() {
    }

    public void initialize(ReviewInfoRepository reviewInfoRepository) {
        this.reviewInfoRepository = reviewInfoRepository;
        PaperRecord.initialize(assignedCount());
    }

    @Override
    public boolean allocatePapers(Conference conference) {

        final List<ReviewerRecord> reviewerRecords = ReviewerRecord.project(conference.getPcMembers());
        final List<PaperRecord> paperRecords = PaperRecord.project(conference.getPapers());

        bind(reviewerRecords, paperRecords);

        ReviewerRecordContainer reviewerRecordContainer = new ReviewerRecordContainer(reviewerRecords);
        PaperRecordContainer paperRecordContainer = new PaperRecordContainer(paperRecords);
        Set<AssignmentRecord> assignmentRecords = new HashSet<>();

        int[] counts = averageCounts(reviewerRecords.size(),
                paperRecords.size() * assignedCount());

        int index = 0;
        AssignmentRecord record;
        ReviewerRecord currentRecord;
        while (null != (currentRecord = reviewerRecordContainer.poll())) {
            record = randomAllocate(currentRecord, paperRecordContainer, counts[index++]);
            if (record == null) return false;
            assignmentRecords.add(record);

            reviewerRecordContainer.reorganize();
            paperRecordContainer.reorganize();
        }

        Collection<ReviewInfo> reviewInfos = AssignmentRecord.aggregateTasks(assignmentRecords);
        reviewInfoRepository.saveAll(reviewInfos);

        return true;
    }

    private static int[] averageCounts(int size, int total) {
        int[] counts = new int[size];
        int r = total % size;
        int q = total / size;
        for (int i = 0; i < size; i++) {
            if (i >= size - r) {
                counts[i] = q + 1;
            } else {
                counts[i] = q;
            }
        }
        return counts;
    }

    private static AssignmentRecord randomAllocate(
            ReviewerRecord reviewerRecord,
            PaperRecordContainer container, int selected) {

        AssignmentRecord record = new AssignmentRecord(reviewerRecord);

        PaperRecord fetched;
        List<PaperRecord> assigned = new ArrayList<>(selected);

        while (assigned.size() != selected && !container.isEmpty()) {
            fetched = container.poll();
            if (fetched != null && !isWriterOfPaper(reviewerRecord, fetched)) {
                assigned.add(fetched);
            }
        }

        if (assigned.size() != selected) return null;

        reviewerRecord.assign(assigned);
        if (!reviewerRecord.reduce()) return null;

        record.addAllAssignments(assigned);
        return record;
    }

    private static void bind(Collection<ReviewerRecord> reviewerRecords, Collection<PaperRecord> paperRecords) {
        for (ReviewerRecord reviewerRecord : reviewerRecords) {
            reviewerRecord.bind(paperRecords);
        }
    }


    private static boolean isWriterOfPaper(ReviewerRecord reviewerRecord, PaperRecord paperRecord) {
        if (null == reviewerRecord || null == paperRecord) {
            return false;
        }
        final List<ReviewerRecord> authors = paperRecord.authors;
        for (ReviewerRecord author : authors) {
            if (author == reviewerRecord) {
                return true;
            }
        }
        return false;
    }

    private static class ReviewerRecord implements Comparable<ReviewerRecord> {
        private final PcMember reviewer;
        private final List<PaperRecord> works = new LinkedList<>();
        private final List<PaperRecord> assignments = new LinkedList<>();

        private ReviewerRecord(PcMember reviewer) {
            this.reviewer = reviewer;
        }

        void bind(Collection<PaperRecord> paperRecords) {
            for (PaperRecord paperRecord : paperRecords) {
                if (isWriterOfPaper(reviewer, paperRecord.paper)) {
                    this.works.add(paperRecord);
                    paperRecord.authors.add(this);
                }
            }
        }

        void assign(PaperRecord paperRecord) {
            this.assignments.add(paperRecord);
            --paperRecord.remains;
        }

        void assign(Collection<PaperRecord> paperRecords) {
            for (PaperRecord paperRecord : paperRecords) {
                assign(paperRecord);
            }
        }

        boolean reduce() {
            for (PaperRecord work : works) {
                if (!work.authors.remove(this)) {
                    return false;
                }
            }
            for (PaperRecord assignment : assignments) {
                if (!assignment.reduce()) {
                    return false;
                }
            }
            return true;
        }

        private int contribution() {
            return works.size();
        }

        private static boolean isWriterOfPaper(PcMember reviewer, Paper paper) {
            final String rRealname = reviewer.getUser().getRealname();
            final String rEmail = reviewer.getUser().getEmail();

            final List<Writer> writers = paper.getWriters();
            for (Writer writer : writers) {
                if (rRealname.equals(writer.getRealname()) &&
                        rEmail.equals(writer.getEmail())) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public int compareTo(ReviewerRecord o) {
            return o.contribution() - this.contribution();
        }

        static List<ReviewerRecord> project(Collection<PcMember> reviewers) {
            final ArrayList<ReviewerRecord> list = new ArrayList<>(reviewers.size());
            for (PcMember reviewer : reviewers) {
                list.add(new ReviewerRecord(reviewer));
            }
            return list;
        }

    }

    private static class PaperRecord implements Comparable<PaperRecord> {
        private static int copies;

        private int remains = copies;
        private final Paper paper;
        private final List<ReviewerRecord> authors = new LinkedList<>();

        static void initialize(int remains) {
            if (remains <= 0) {
                throw new IllegalArgumentException("remains must be positive.");
            }
            copies = 3;
        }

        private PaperRecord(Paper paper) {
            this.paper = paper;
        }

        boolean reduce() {
            if (0 == remains) {
                for (ReviewerRecord author : authors) {
                    if (!author.works.remove(this)) {
                        return false;
                    }
                }
                --remains;
            }

            return true;
        }

        boolean isExhausted() {
            return remains <= 0;
        }

        private int writers() {
            return this.authors.size();
        }


        @Override
        public int compareTo(PaperRecord o) {
            final int deltaWriters = this.writers() - o.writers();
            final int deltaRemains = o.remains - this.remains;
            return (deltaWriters == 0) ? deltaRemains : deltaWriters;
        }

        static List<PaperRecord> project(Collection<Paper> papers) {
            final ArrayList<PaperRecord> list = new ArrayList<>(papers.size());
            for (Paper paper : papers) {
                list.add(new PaperRecord(paper));
            }
            return list;
        }
    }


    private static class ReviewerRecordContainer {
        private PriorityQueue<ReviewerRecord> queue;

        ReviewerRecordContainer(List<ReviewerRecord> reviewerRecords) {
            this.queue = new PriorityQueue<>(reviewerRecords);
        }

        ReviewerRecord poll() {
            return this.queue.poll();
        }

        boolean isEmpty() {
            return this.queue.isEmpty();
        }

        void reorganize() {
            this.queue = new PriorityQueue<>(queue);
        }
    }

    private static class PaperRecordContainer {
        private final List<PaperRecord> paperRecords;
        private PriorityQueue<PaperRecord> queue;

        PaperRecordContainer(List<PaperRecord> paperRecords) {
            this.paperRecords = paperRecords;
            this.queue = new PriorityQueue<>(paperRecords);
        }

        PaperRecord poll() {
            return queue.poll();
        }

        boolean isEmpty() {
            return this.queue.isEmpty();
        }

        void reorganize() {
            paperRecords.removeIf(PaperRecord::isExhausted);
            this.queue = new PriorityQueue<>(paperRecords);
        }
    }


    private static class AssignmentRecord {
        private final ReviewerRecord reviewerRecord;
        private final List<PaperRecord> assignments = new ArrayList<>();

        public AssignmentRecord(ReviewerRecord reviewerRecord) {
            this.reviewerRecord = reviewerRecord;
        }

        void addAssignment(PaperRecord paper) {
            this.assignments.add(paper);
        }

        void addAllAssignments(Collection<PaperRecord> paperRecords) {
            for (PaperRecord paperRecord : paperRecords) {
                addAssignment(paperRecord);
            }
        }

        Collection<ReviewInfo> assignTasks() {

            HashSet<ReviewInfo> hashSet = new HashSet<>();

            for (PaperRecord assignment : assignments) {
                hashSet.add(new ReviewInfo(assignment.paper, reviewerRecord.reviewer));
            }

            return hashSet;
        }

        static Collection<ReviewInfo> aggregateTasks(Collection<AssignmentRecord> records) {
            HashSet<ReviewInfo> hashSet = new HashSet<>();
            for (AssignmentRecord record : records) {
                hashSet.addAll(record.assignTasks());
            }
            return hashSet;
        }
    }

}
